﻿using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using WholeSlideImageLibrary.DeepZoom;

namespace WholeSlideImageLibrary.Formats.DeepZoomDirectory
{
    internal class DeepZoomDirectoryDeepZoomTileSource : InternalDeepZoomTileSource
    {
        private readonly DeepZoomDirectoryWholeSlideImage _image;
        private readonly DeepZoomManifest _manifest;
        private readonly EncodedImageFileFormat _fileFormat;

        private readonly DeepZoomLayer[] _layers;

        public DeepZoomDirectoryDeepZoomTileSource(DeepZoomDirectoryWholeSlideImage image, DeepZoomManifest manifest, EncodedImageFileFormat fileFormat)
        {
            _image = image;
            _manifest = manifest;
            _fileFormat = fileFormat;
            _layers = DeepZoomHelper.CalculateDeepZoomLayers(manifest.Width, manifest.Height);
        }

        public override int Width => _manifest.Width;

        public override int Height => _manifest.Height;

        public override int TileSize => _manifest.TileSize;

        public override int Overlap => _manifest.Overlap;

        public override EncodedImageFileFormat TileFileFormat => _fileFormat;

        public override bool CheckTileExists(int layer, int column, int row)
        {
            DeepZoomLayer[]? layers = _layers;
            if ((uint)layer >= (uint)layers.Length)
            {
                return false;
            }
            DeepZoomLayer layerInfo = layers[layer];

            int tileSize = _manifest.TileSize;
            int horizontalTileCount = (layerInfo.Width + tileSize - 1) / tileSize;
            int verticalTileCount = (layerInfo.Height + tileSize - 1) / tileSize;
            if ((uint)column >= (uint)horizontalTileCount)
            {
                return false;
            }
            if ((uint)row >= (uint)verticalTileCount)
            {
                return false;
            }
            return true;
        }

        private const int MaximumSegmentSize = 16384;

        public override async Task WriteToStreamAsync(int layer, int column, int row, Stream stream, CancellationToken cancellationToken)
        {
            FileSourceInfo? tile = await _image.GetTileAsync(layer, column, row).ConfigureAwait(false);
            if (tile is null)
            {
                throw new InvalidDataException();
            }

            long offset = 0;
            long length = tile.FileSize;
            FileSourceReader? reader = null;
            byte[] buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min(length, MaximumSegmentSize));
            try
            {
                reader = await tile.OpenAsync(cancellationToken).ConfigureAwait(false);

                while (length != 0)
                {
                    int bytesToRead = (int)Math.Min(length, MaximumSegmentSize);
                    int bytesRead = await reader.ReadAsync(offset, buffer.AsMemory(0, bytesToRead), cancellationToken).ConfigureAwait(false);
                    if (bytesRead != bytesToRead)
                    {
                        throw new InvalidDataException();
                    }

                    await stream.WriteAsync(buffer, 0, bytesToRead, cancellationToken).ConfigureAwait(false);
                    offset += bytesToRead;
                    length -= bytesToRead;
                }

            }
            finally
            {
                ArrayPool<byte>.Shared.Return(buffer);
                if (!(reader is null))
                {
                    await reader.DisposeAsync().ConfigureAwait(false);
                }
            }
        }

        public override async Task WriteToBufferAsync(int layer, int column, int row, IBufferWriter<byte> writer, CancellationToken cancellationToken)
        {
            FileSourceInfo? tile = await _image.GetTileAsync(layer, column, row).ConfigureAwait(false);
            if (tile is null)
            {
                throw new InvalidDataException();
            }

            long offset = 0;
            long length = tile.FileSize;
            FileSourceReader? reader = null;
            try
            {
                reader = await tile.OpenAsync(cancellationToken).ConfigureAwait(false);

                while (length != 0)
                {
                    int bytesToRead = (int)Math.Min(length, MaximumSegmentSize);
                    Memory<byte> memory = writer.GetMemory(bytesToRead);
                    bytesToRead = (int)Math.Min(length, memory.Length);
                    int bytesRead = await reader.ReadAsync(offset, memory.Slice(0, bytesToRead), cancellationToken).ConfigureAwait(false);
                    if (bytesRead != bytesToRead)
                    {
                        throw new InvalidDataException();
                    }

                    writer.Advance(bytesToRead);
                    offset += bytesToRead;
                    length -= bytesToRead;
                }
            }
            finally
            {
                if (!(reader is null))
                {
                    await reader.DisposeAsync().ConfigureAwait(false);
                }
            }
        }
    }
}
